"""Trigger for Koji/Brew builds."""
import argparse
import json
import os
import re
import typing

from cki_lib import cki_pipeline
from cki_lib import config_tree
from cki_lib import gitlab
from cki_lib import logger
from cki_lib import messagequeue
from cki_lib import metrics
from cki_lib import misc
from cki_lib import yaml
import koji
import prometheus_client as prometheus
import sentry_sdk

LOGGER = logger.get_logger('cki_tools.koji_trigger')

METRIC_PIPELINE_TRIGGERED = prometheus.Counter(
    'cki_koji_pipeline_triggered', 'Number of Koji/Brew pipelines triggered')


class KojiTrigger:
    """Trigger for Koji/Brew builds."""

    # pylint: disable=too-many-arguments,too-many-positional-arguments
    def __init__(self, config, gitlab_url, koji_server_url, koji_web_url, koji_top_url):
        """Initialize instance."""
        self.config = config
        self.gl_instance = gitlab.get_instance(gitlab_url)
        self.koji_server_url = koji_server_url
        self.koji_web_url = koji_web_url
        self.koji_top_url = koji_top_url

    @staticmethod
    def get_nvr(koji_session, task_data):
        """Try to get the NVR from the task tree."""
        task_id = task_data['id']
        request_string = task_data['request'][0]
        rpm_match = re.search(r'/rpms/(.*)(?:.git)?\??#', request_string)
        # Handle official builds from dist-git
        if rpm_match:
            LOGGER.info('%s: Build from dist-git found!', task_id)
            # These don't contain full NVR in the request, just "kernel", and
            # we need to get it from the task itself.
            build_info = koji_session.listBuilds(taskID=task_id)
            if build_info:
                return build_info[0]['nvr']

        # Handle scratch builds from SRPM
        nvr_match = re.search(r'.*/(.*\.rpm)', request_string)
        if nvr_match:
            return nvr_match.group(1)

        # Handle all the rest -- scratch builds from dist git or git trees
        for child in koji_session.listTasks(
                opts={'parent': task_id, 'method': 'buildArch', 'decode': True},
                queryOpts={'limit': 1}):
            return os.path.basename(child['request'][0])

        LOGGER.warning('%s: Can\'t get NVR from task: %s', task_id, request_string)
        return ''

    def get_triggers(self, task_id: int) -> typing.List[typing.Dict[str, typing.Any]]:
        # pylint: disable=too-many-locals,too-many-branches,too-many-statements
        """Determine the trigger variables."""
        triggers: typing.List[typing.Dict[str, typing.Any]] = []

        koji_session = koji.ClientSession(self.koji_server_url)
        task_data = koji_session.getTaskInfo(task_id, request=True)
        if not task_data.get('request'):
            LOGGER.info('%s: Task doesn\'t have a build request, ignoring', task_id)
            return triggers
        if not (nvr := self.get_nvr(koji_session, task_data)):
            return triggers
        task_options = task_data['request'][2]

        LOGGER.info('%s: Build %s found!', task_id, nvr)

        for pipeline_config in config_tree.process_config_tree(self.config).values():
            source_package_name = pipeline_config['source_package_name']
            rpm_release = pipeline_config.get('rpm_release', '')
            if not re.search(rf'^{source_package_name}-\d+[.-](\S+[.-])+{rpm_release}', nvr):
                continue

            variables = pipeline_config.copy()

            variables['server_url'] = self.koji_server_url
            variables['top_url'] = self.koji_top_url
            variables['web_url'] = self.koji_web_url

            LOGGER.info('%s: Found possible trigger', task_id)
            scratch_build = task_options.get('scratch') or task_options.get('skip_tag')
            if scratch_build and not misc.strtobool(variables.get('.test_scratch', 'False')):
                LOGGER.info('%s: Scratch build testing disabled in trigger', task_id)
                continue

            # If the build comes from automation, the real owner is added to the
            # metadata. Prefer this information as a build owner as opposed to the
            # automation account.
            # Documentation placeholder: https://issues.redhat.com/browse/OSCI-4190
            owner_name = misc.get_nested_key(task_options,
                                             'custom_user_metadata/osci/upstream_owner_name')
            if not owner_name:
                owner_name = koji_session.getUser(task_data['owner'])['name']
            variables['submitter'] = f'{owner_name}@redhat.com'
            variables['checkout_contacts'] = json.dumps([variables['submitter']])

            variables['scratch'] = misc.booltostr(scratch_build)
            variables['officialbuild'] = misc.booltostr(not scratch_build)

            koji_arches = task_options.get('arch_override')
            if koji_arches:
                if 'architectures' in variables:
                    # Override Koji's override by user's, but only for arches that
                    # were actually built. Split out the code to multiple lines so
                    # we actually understand what's going on...
                    koji_arch_set = set(koji_arches.split())
                    user_arch_set = set(variables['architectures'].split())
                    new_arches = user_arch_set & koji_arch_set
                    variables['architectures'] = ' '.join(new_arches)
                else:
                    variables['architectures'] = koji_arches

            kernel_version = nvr.removeprefix(f'{source_package_name}-')
            kernel_version = kernel_version.removesuffix('.src.rpm')

            # automatically determine rhel-Y and rhel-Y.Z from releases such as .el8 and .el8_2
            if ('kcidb_tree_name' not in variables) and ('.el' in kernel_version):
                variables['kcidb_tree_name'] = re.sub(
                    r'.*\.el(\d+)(?:_(\d+))?.*', lambda m:
                    f'rhel-{m[1]}' + (f'.{m[2]}' if m[2] else ''),
                    kernel_version,
                )

            variables['kernel_version'] = kernel_version
            variables['brew_task_id'] = str(task_id)
            variables['title'] = f'Koji/Brew: Task {task_id} - {variables["package_name"]}'
            if task_options.get('scratch'):
                variables['title'] += ' (scratch)'

            if report_rules := variables.get('.report_rules'):
                variables['report_rules'] = json.dumps(report_rules)

            triggers.append(config_tree.clean_config(variables))

        if not triggers:
            LOGGER.info('%s: Pipeline for %s not configured!', task_id, nvr)
        return triggers

    @staticmethod
    def parse_message(body):
        """Extract a task_id from a message."""
        if not (
                # org.fedoraproject.prod.buildsys.build.state.change for non-scratch builds
                ((task_info := body.get('task')) and (body.get('new') == 1)) or
                # org.fedoraproject.prod.buildsys.task.state.change for scratch builds
                # eng.brew.task.closed for both scratch and non-scratch builds
                ((task_info := body.get('info')) and (body.get('new') == 'CLOSED'))
        ):
            return None

        # pylint: disable=too-many-boolean-expressions
        if (
                body.get('attribute') != 'state' or
                not task_info.get('request') or
                task_info.get('method') != 'build' or
                not (task_id := task_info.get('id'))
        ):
            return None

        LOGGER.info('%s: A build completed!', task_id)
        return task_id

    def callback(self, body=None, **_):
        """Process received messages."""
        task_id = self.parse_message(body)
        if task_id and (triggers := self.get_triggers(task_id)):
            cki_pipeline.trigger_multiple(self.gl_instance, triggers)
            METRIC_PIPELINE_TRIGGERED.inc(len(triggers))

    def manual(self, task_id, variables):
        """Interactively trigger a pipeline for a fixed task_id."""
        for trigger in self.get_triggers(task_id):
            gl_project = cki_pipeline.pipeline_project(self.gl_instance, trigger)
            gl_pipeline = cki_pipeline.trigger(gl_project, trigger,
                                               variable_overrides=variables,
                                               interactive=True,
                                               non_production_delay_s=0)  # interactive use
            print(f'Pipeline: {gl_pipeline.web_url}')


def main(args: typing.Optional[typing.List[str]] = None) -> None:
    """Run it."""
    parser = argparse.ArgumentParser()
    parser.add_argument('--gitlab-url', default=os.environ.get('GITLAB_URL'),
                        help='GitLab URL')
    parser.add_argument('--koji-server-url', default=os.environ.get('KOJI_SERVER_URL'),
                        help='URL of XMLRPC server')
    parser.add_argument('--koji-web-url', default=os.environ.get('KOJI_WEB_URL'),
                        help='URL of the Koji web interface')
    parser.add_argument('--koji-top-url', default=os.environ.get('KOJI_TOP_URL'),
                        help='URL for Koji file access')
    parser.add_argument('--config', default=os.environ.get('KOJI_TRIGGER_CONFIG'),
                        help='YAML configuration file to use')
    parser.add_argument('--config-path', default=os.environ.get('KOJI_TRIGGER_CONFIG_PATH'),
                        help='Path to YAML configuration file')
    parser.add_argument('--task-id', help='Koji/Brew task ID')
    parser.add_argument('--variables', action=misc.StoreNameValuePair, default={},
                        metavar='KEY=VALUE', help='Override trigger variables')
    parsed_args = parser.parse_args(args)

    config = yaml.load(
        contents=parsed_args.config,
        file_path=parsed_args.config_path,
        resolve_references=True,
    )

    receiver = KojiTrigger(config, parsed_args.gitlab_url, parsed_args.koji_server_url,
                           parsed_args.koji_web_url, parsed_args.koji_top_url)

    if parsed_args.task_id:
        receiver.manual(parsed_args.task_id, parsed_args.variables)
    else:
        metrics.prometheus_init()
        messagequeue.MessageQueue().consume_messages(
            os.environ.get('WEBHOOK_RECEIVER_EXCHANGE', 'cki.exchange.webhooks'),
            os.environ['KOJI_TRIGGER_ROUTING_KEYS'].split(),
            receiver.callback,
            queue_name=os.environ['KOJI_TRIGGER_QUEUE'],
        )


if __name__ == '__main__':
    misc.sentry_init(sentry_sdk)
    main()
